package com.simple.chinagpay.service.impl;

import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.simple.chinagpay.api.PayApi;
import com.simple.chinagpay.config.PayConfig;
import com.simple.chinagpay.service.PayService;
import com.simple.common.entity.constant.Payment;
import com.simple.common.entity.dto.pay.vm.*;
import com.simple.common.entity.po.sys.ChannelComp;
import com.simple.common.entity.po.sys.Trade;
import com.simple.common.framework.context.Contexts;
import com.simple.common.framework.retrofit.Retrofits;
import com.simple.common.framework.scale.Scale;
import com.simple.common.utils.CountryCodeUtil;
import com.simple.ds.service.TradeApi;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;

import java.io.IOException;
import java.math.BigDecimal;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

/**
 * @author yasud
 * @date 2021年03月09日 09:40
 * @description
 */
@Slf4j
@Service
public class PayServiceImpl implements PayService {

    @Autowired
    PayConfig config;

    @Autowired
    TradeApi tradeApi;

    @Autowired
    ObjectMapper objectMapper;

    @Override
    public DirectPayRspDto pay(DirectPayDto dto) {
        Trade trade = dto.getTrade();
        risk(trade, dto.getCardInfo());
        ChannelComp channel = dto.getChannel();
        String apiHost = channel.getAsString("apiHost");
        Map<String, Object> map = generateBody(trade, channel, dto.getCardInfo());
        Retrofit retrofit = Retrofits.SINGLE.createRetrofit(apiHost);
        PayApi payApi = retrofit.create(PayApi.class);
        Call<ResponseBody> call = payApi.directPay(Mono.fromCallable(() -> {
            String s = CUtils.formatJson(Contexts.OBJECTMAPPER.writeValueAsString(map));
            return RequestBody.create(MediaType.get("application/json;charset=utf-8"), s);
        }).block());
        Map<String, Object> result;
        try {
            Response<ResponseBody> response = call.execute();
            if (response.isSuccessful()) {
                result = Contexts.OBJECTMAPPER.readValue(response.body().string(), Map.class);
            } else {
                result = Contexts.OBJECTMAPPER.readValue(response.errorBody().string(), Map.class);
            }
        } catch (IOException e) {
            trade.setStatus(Trade.Status.PENDING);
            trade.setMsg(Payment.TRADE_STATUS_DESCRIBE.EVEN_SERIAL_STATUS_PAYMENT_PENDING_MSG);
            trade.setChannelStatus("500");
            trade.setChannelMsg("超时");
            DirectPayRspDto rspDto = new DirectPayRspDto();
            rspDto.setTrade(trade);
            rspDto.setPayType(Payment.DIRECT_PAY_TYPE.DIRECT_PAYMENT_TYPE_2D);
            return rspDto;
        }
        String code = (String) result.get("code");
        Preconditions.checkState("00".equalsIgnoreCase(code), result.get("message"));
        trade.putToRemark1("cardHolder", dto.getCardInfo().getCardFristName() + "_" + dto.getCardInfo().getCardLastName());
        trade.putToRemark1("cardNo", dto.getCardInfo().getCardNo());
        Map<String, Object> data = (Map<String, Object>) result.get("data");
        String resultCode = (String) data.get("resultCode");
        if ("00".equals(resultCode)) {
            trade.setStatus(Trade.Status.SUCCESS);
            trade.setMsg(Payment.TRADE_STATUS_DESCRIBE.EVEN_SERIAL_STATUS_PAYMENT_SUCCESS_MSG);
            trade.setChannelStatus(resultCode);
            trade.setChannelMsg(Payment.TRADE_STATUS_DESCRIBE.EVEN_SERIAL_STATUS_PAYMENT_SUCCESS_MSG);
            trade.setChannelOrderNo((String) data.get("transNo"));
        } else if ("98".equals(resultCode)) {
            trade.setStatus(Trade.Status.PENDING);
            trade.setMsg(Payment.TRADE_STATUS_DESCRIBE.EVEN_SERIAL_STATUS_PAYMENT_PENDING_MSG);
            trade.setChannelStatus(resultCode);
            trade.setChannelMsg(Payment.TRADE_STATUS_DESCRIBE.EVEN_SERIAL_STATUS_PAYMENT_PENDING_MSG);
            trade.setChannelOrderNo((String) data.get("transNo"));
        } else {
            trade.setStatus(Trade.Status.FAILED);
            trade.setMsg(Payment.TRADE_STATUS_DESCRIBE.EVEN_SERIAL_STATUS_PAYMENT_FAILURE_MSG);
            trade.setChannelStatus(resultCode);
            trade.setChannelMsg(Payment.TRADE_STATUS_DESCRIBE.EVEN_SERIAL_STATUS_PAYMENT_FAILURE_MSG);
            trade.setChannelOrderNo((String) data.get("transNo"));
            String resultMessage = (String) data.get("resultMessage");
            if (StringUtils.isNotBlank(resultMessage)) {
                trade.setMsg(resultMessage);
                trade.setChannelMsg(resultMessage);
            }

        }
        DirectPayRspDto rspDto = new DirectPayRspDto();
        rspDto.setTrade(trade);
        rspDto.setPayType(Payment.DIRECT_PAY_TYPE.DIRECT_PAYMENT_TYPE_2D);
        return rspDto;
    }

    @Override
    @SneakyThrows
    public RedirectPayRspDto redirectPay(RedirectPayDto dto) {
        Trade trade = dto.getTrade();
        ChannelComp channel = dto.getChannel();
        String apiHost = channel.getAsString("apiHost");
        Map<String, Object> map = generateRedirectPayBody(trade, channel);
        Retrofit retrofit = Retrofits.SINGLE.createRetrofit(apiHost);
        PayApi payApi = retrofit.create(PayApi.class);
        Call<ResponseBody> call = payApi.redirectPay(map);
        Map<String, Object> result;
        try {
            Response<ResponseBody> response = call.execute();
            if (response.isSuccessful()) {
                result = Contexts.OBJECTMAPPER.readValue(response.body().string(), Map.class);
            } else {
                result = Contexts.OBJECTMAPPER.readValue(response.errorBody().string(), Map.class);
            }
        } catch (IOException e) {
            trade.setStatus(Trade.Status.PENDING);
            trade.setMsg(Payment.TRADE_STATUS_DESCRIBE.EVEN_SERIAL_STATUS_PAYMENT_PENDING_MSG);
            trade.setChannelStatus("500");
            trade.setChannelMsg("超时");
            RedirectPayRspDto rspDto = new RedirectPayRspDto();
            rspDto.setTrade(trade);
            return rspDto;
        }
        String code = (String) result.get("code");
        Preconditions.checkState("00".equalsIgnoreCase(code), result.get("message"));
        Map<String, Object> data = (Map<String, Object>) result.get("data");
        String resultCode = (String) data.get("resultCode");
        if ("00".equals(resultCode)) {
            trade.setStatus(Trade.Status.SUCCESS);
            trade.setMsg(Payment.TRADE_STATUS_DESCRIBE.EVEN_SERIAL_STATUS_PAYMENT_SUCCESS_MSG);
            trade.setChannelStatus(resultCode);
            trade.setChannelMsg(Payment.TRADE_STATUS_DESCRIBE.EVEN_SERIAL_STATUS_PAYMENT_SUCCESS_MSG);
            trade.setChannelOrderNo((String) data.get("transNo"));
        } else if ("98".equals(resultCode)) {
            trade.setStatus(Trade.Status.PENDING);
            trade.setMsg(Payment.TRADE_STATUS_DESCRIBE.EVEN_SERIAL_STATUS_PAYMENT_PENDING_MSG);
            trade.setChannelStatus(resultCode);
            trade.setChannelMsg(Payment.TRADE_STATUS_DESCRIBE.EVEN_SERIAL_STATUS_PAYMENT_PENDING_MSG);
            trade.setChannelOrderNo((String) data.get("transNo"));
        } else {
            trade.setStatus(Trade.Status.FAILED);
            trade.setMsg(Payment.TRADE_STATUS_DESCRIBE.EVEN_SERIAL_STATUS_PAYMENT_FAILURE_MSG);
            trade.setChannelStatus(resultCode);
            trade.setChannelMsg(Payment.TRADE_STATUS_DESCRIBE.EVEN_SERIAL_STATUS_PAYMENT_FAILURE_MSG);
            trade.setChannelOrderNo((String) data.get("transNo"));
            String resultMessage = (String) data.get("resultMessage");
            if (StringUtils.isNotBlank(resultMessage)) {
                trade.setMsg(resultMessage);
                trade.setChannelMsg(resultMessage);
            }

        }
        RedirectPayRspDto rspDto = new RedirectPayRspDto();
        rspDto.setTrade(trade);
        rspDto.setPayHtml("");
        return rspDto;
    }

    /**
     * 规则
     *
     * @param trade
     * @param cardInfo
     */
    @SneakyThrows
    private void risk(Trade trade, CardInfo cardInfo) {
        BigDecimal v = Scale.scale(trade.getPayAmount());
        Preconditions.checkState(v.compareTo(Scale.scale("3")) >= 1 && v.compareTo(Scale.scale("7000")) <= -1, "amount not in allow area");
        int byRiskDoNotHour = tradeApi.findByRiskDoNotHour(cardInfo.getCardNo(), cardInfo.getCardFristName() + "_" + cardInfo.getCardLastName(), "59",
                DateUtils.addDays(new Date(), -1), trade.getChannelId());
        Preconditions.checkState(byRiskDoNotHour <= 0, "Do not honour");
        int byRiskConfigError = tradeApi.findByRiskConfigError(cardInfo.getCardNo(), "43", DateUtils.addDays(new Date(), -1), trade.getChannelId());
        Preconditions.checkState(byRiskConfigError <= 0, "Config error");
        int byRiskInvalidTransaction = tradeApi.findByRiskInvalidTransaction(cardInfo.getCardFristName() + "_" + cardInfo.getCardLastName(), "44", DateUtils.addDays(new Date(), -1), trade.getChannelId());
        Preconditions.checkState(byRiskInvalidTransaction <= 0, "Invalid transaction");
        int byRiskConfigError2 = tradeApi.findByRiskConfigError(cardInfo.getCardNo(), "53", DateUtils.addDays(new Date(), -1), trade.getChannelId());
        Preconditions.checkState(byRiskConfigError2 <= 0, "Not sufficient funds");
        int byRiskRestrictedCard = tradeApi.findByRiskRestrictedCard("61", cardInfo.getCardNo(), cardInfo.getCardFristName() + "_" + cardInfo.getCardLastName(), DateUtils.addDays(new Date(), -1), trade.getChannelId());
        Preconditions.checkState(byRiskRestrictedCard <= 0, "Transaction not permitted to cardholder Card");
        String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        Map map1 = Contexts.OBJECTMAPPER.readValue(trade.getExtraInfo(), Map.class);
        String ipAddress = "";
        if (MapUtils.isNotEmpty(map1)) {
            ipAddress = (String) map1.getOrDefault("ipAddress", "");
        }
        int byRiskCardHolderCount = tradeApi.findByRiskCardHolderCount(ipAddress, cardInfo.getCardNo(), cardInfo.getCardFristName() + "_" + cardInfo.getCardLastName(), date, trade.getChannelId());
        Preconditions.checkState(byRiskCardHolderCount < 5, "Request reject.card over limit by success");
    }

    @SneakyThrows
    private Map<String, Object> generateBody(Trade trade,
                                             ChannelComp channel,
                                             CardInfo cardInfo) {
        Map<String, Object> billingInfo = Contexts.OBJECTMAPPER.readValue(trade.getBillingInfo(), Map.class);
        Map<String, Object> shippingInfo = Contexts.OBJECTMAPPER.readValue(trade.getShippingInfo(), Map.class);
        String aesKey = channel.getAsString("aesKey");
        String secretKey = channel.getAsString("secretKey");
        String mid = channel.getAsString("mid");
        String chCountry = CountryCodeUtil.TwoCode2ThreeCOde(billingInfo.getOrDefault("billCountry", "").toString());
        Preconditions.checkState(!"000".equalsIgnoreCase(chCountry), "billingCountry not support");
        String recCountry = CountryCodeUtil.TwoCode2ThreeCOde(shippingInfo.getOrDefault("shippingCountry", "").toString());
        Preconditions.checkState(!"000".equalsIgnoreCase(recCountry), "shippingCountry not support");

        Map<String, Object> param = Maps.newHashMap();
        param.put("merchantId", mid + "");
        param.put("orderTime", DateFormatUtils.format(trade.getCreateTime(), "yyyyMMddHHmmss"));

        param.put("orderId", trade.getId() + "");
        param.put("currency", trade.getPayAmountCurrency());
        param.put("amount", new BigDecimal(trade.getPayAmount()).multiply(new BigDecimal("100")).intValue() + "");
        param.put("orderDetails", "orderDetails");

        param.put("chCountry", chCountry);
        param.put("chState", billingInfo.getOrDefault("billState", "").toString());
        param.put("chCity", billingInfo.getOrDefault("billCity", "").toString());
        param.put("chAddress", billingInfo.getOrDefault("billAddress", "").toString());
        param.put("chPostCode", billingInfo.getOrDefault("billZipcode", "").toString());
        param.put("chFirstName", billingInfo.getOrDefault("billFirstName", "").toString());
        param.put("chLastName", billingInfo.getOrDefault("billLastName", "").toString());
        param.put("chEmail", billingInfo.getOrDefault("billEmail", "").toString());
        param.put("chPhone", billingInfo.getOrDefault("billTel", "").toString());

        param.put("recCountry", recCountry);
        param.put("recState", shippingInfo.getOrDefault("shippingState", "").toString());
        param.put("recCity", shippingInfo.getOrDefault("shippingCity", "").toString());
        param.put("recAddress", shippingInfo.getOrDefault("shippingAddress", "").toString());
        param.put("recPostCode", shippingInfo.getOrDefault("shippingZipcode", "").toString());
        param.put("recPhone", shippingInfo.getOrDefault("shippingTel", "").toString());
        param.put("recFirstName", shippingInfo.getOrDefault("shippingFirstName", "").toString());
        param.put("recLastName", shippingInfo.getOrDefault("shippingLastName", "").toString());
        param.put("recEmail", "");

        AES aes = SecureUtil.aes(Hex.decodeHex(aesKey));
        param.put("accNo", aes.encryptHex(cardInfo.getCardNo()));
        param.put("expired", cardInfo.getCardExYear() + cardInfo.getCardExMonth());
        param.put("cvv", aes.encryptHex(cardInfo.getCardCvv()));

        param.put("chIp", trade.getFromExtra("ipAddress") + "");
        param.put("backUrl", config.getPlatformBasePath() + "/api/receiver/dynamic/call/" + trade.getId());
        param.put("remark", "");
        param.put("algorithm", "MD5");

        param.put("signature", Mono.fromCallable(() -> {
            TreeMap tm = Maps.newTreeMap();
            tm.putAll(param);
            String s = Contexts.OBJECTMAPPER.writeValueAsString(tm) + secretKey;
            String md5 = DigestUtils.md5Hex(s);
            return md5;
        }).block());
        log.info("构建渠道参数完毕,参数为：{}", param);
        return param;
    }

    @SneakyThrows
    private Map<String, Object> generateRedirectPayBody(Trade trade,
                                                        ChannelComp channel) {
        Map<String, Object> billingInfo = Contexts.OBJECTMAPPER.readValue(trade.getBillingInfo(), Map.class);
        Map<String, Object> shippingInfo = Contexts.OBJECTMAPPER.readValue(trade.getShippingInfo(), Map.class);
        String secretKey = channel.getAsString("secretKey");
        String mid = channel.getAsString("mid");
        String chCountry = CountryCodeUtil.TwoCode2ThreeCOde(billingInfo.getOrDefault("billCountry", "").toString());
        Preconditions.checkState(!"000".equalsIgnoreCase(chCountry), "billingCountry not support");
        String recCountry = CountryCodeUtil.TwoCode2ThreeCOde(shippingInfo.getOrDefault("shippingCountry", "").toString());
        Preconditions.checkState(!"000".equalsIgnoreCase(recCountry), "shippingCountry not support");

        Map<String, Object> param = Maps.newHashMap();
        param.put("version", "2.1");
        param.put("merchantId", mid);
        param.put("orderTime", DateFormatUtils.format(trade.getCreateTime(), "yyyyMMddHHmmss"));
        param.put("orderId", trade.getId() + "");
        param.put("currency", trade.getPayAmountCurrency());
        param.put("amount", new BigDecimal(trade.getPayAmount()).multiply(new BigDecimal("100")).intValue() + "");
        param.put("subject", trade.getFromExtra("productName"));
        param.put("orderDetails", generateParamOrderDetails(trade));
        param.put("backUrl", config.getPlatformBasePath() + "/api/receiver/dynamic/call/" + trade.getId());
        param.put("frontUrl", MessageFormat.format(config.getPlatformBasePath() + "/api/pay/redirect/{0}/{1}", channel.getChannelFlag(), trade.getId() + ""));
        param.put("remark", "");
        param.put("algorithm", "MD5");
        param.put("signature", Mono.fromCallable(() -> {
            TreeMap tm = Maps.newTreeMap();
            tm.putAll(param);
            String s = Contexts.OBJECTMAPPER.writeValueAsString(tm) + secretKey;
            String md5 = DigestUtils.md5Hex(s);
            return md5;
        }).block());
        log.info("构建渠道参数完毕,参数为：{}", param);
        return param;
    }

    /**
     * 链接标题
     * @param trade
     * @return
     */
    @SneakyThrows
    private String concatSubject(Trade trade) {
        List<Map<String, Object>> maps = objectMapper.readValue(trade.getGoodsList(), new TypeReference<List<Map<String, Object>>>() {});
        String subject = maps.stream().map(v -> (String)v.get("name")).collect(Collectors.joining(","));
        return subject;
    }

    @SneakyThrows
    private String generateParamOrderDetails(Trade trade) {
        List<Map<String, Object>> maps = objectMapper.readValue(trade.getGoodsList(), new TypeReference<List<Map<String, Object>>>() {});
        List<Map<String, Object>> items = maps.stream().map(v -> {
            Map<String, Object> entry = Maps.newHashMap();
            entry.put("name", v.get("name"));
            entry.put("count", v.get("quantity"));
            entry.put("unit", v.get("sku"));
            entry.put("price", v.get("price"));
            return entry;
        }).collect(Collectors.toList());
        return objectMapper.writeValueAsString(ImmutableMap.of(
                "businessType", "GOODS_TRADE",
                "goodsItem", items
        ));
    }
}
